/**
 * Copyright (c) 2010, Casey Lai
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 * 
 * Neither the name of the Cheetah Group nor the names of its contributors may be
 * used to endorse or promote products derived from this software without specific
 * prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package org.cheetahworkflow.engine;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * this class read the workflow configure file, and build the runtime environment for workflow engine
 * @author caseylai
 * @param <T> context
 */
public class WorkflowConfigure<T> {
	
	// keep node alias and entry pairs. This map is initialized when this class constructing,
	// later on there are only read operations, no modify operations.
	private Map<String, WorkflowNodeEntry<T>> map = new HashMap<String, WorkflowNodeEntry<T>>();
	
	private Constants<T> constants = new Constants<T>();
	
	// root node of configure file document
	private Node root;
	
	// xpath object used to parse the xml document
	private XPath xpath = XPathFactory.newInstance().newXPath();

	WorkflowConfigure(InputStream in) {
		try {
			root = parse(in);
			init();
		} catch (Exception e) {
			throw new WorkflowException(e);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					// ignore
				}
			}
		}
	}
	
	/**
	 * get workflow node entry for corresponding node name
	 */
	public WorkflowNodeEntry<T> getEntry(String nodeName) {
		return map.get(nodeName);
	}
	
	// parse root node for xml document
	private Node parse(InputStream in) {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document doc = builder.parse(in);
			return doc.getDocumentElement();
		} catch (Exception e) {
			throw new WorkflowException(e);
		}
	}
	
	// add workflow node alias and entry into map
	private void init() throws DOMException, XPathExpressionException {
		// add start node and end node
		map.put(Constants.NODE_START, new SequenceNodeEntry<T>(this, constants.getEmptyWorkflowNode(), getNodeContent("/workflow/start")));
		
		// iterate other nodes
		NodeList nodes = getNodeList("/workflow/node");
		int len = nodes.getLength();
		for(int i = 0; i < len; i++) {
			Node node = nodes.item(i);
			String type = getNodeContent("./@type", node);
			WorkflowNodeType nodeType = WorkflowNodeType.parse(type);
			// initialize by node type
			switch (nodeType) {
			case SEQUENCE:
				initSequenceNode(node);
				break;
			case SWITCH:
				initSwitchNode(node);
				break;
			case MODULE:
				initModuleNode(node);
				break;
			}
		}
		
		// iterate end nodes
		NodeList endNodes = getNodeList("/workflow/end");
		len = endNodes.getLength();
		for(int i = 0; i < len; i++) {
			Node node = endNodes.item(i);
			String name = getNodeContent("./@name", node);
			map.put(name, constants.getNullNodeEntry());
		}
	}
	
	private void initSequenceNode(Node node) throws DOMException, XPathExpressionException {
		String className = getNodeContent("./class/@name", node);
		String parameterClassName = null;
		Node parameterClassNode = getNode("./class/parameter", node);
		if (parameterClassNode != null) {
			parameterClassName = parameterClassNode.getTextContent();
		}
		WorkflowNode<T> sequenceNode = makeSequenceNode(className, parameterClassName);
		String nextNodeName = getNodeContent("./next-node", node);
		WorkflowNodeEntry<T> entry = new SequenceNodeEntry<T>(this, sequenceNode, nextNodeName);
		String name = node.getAttributes().getNamedItem("name").getNodeValue();
		map.put(name, entry);
	}
	
	private void initSwitchNode(Node node) throws DOMException, XPathExpressionException {
		NodeList nodes = getNodeList("./switch", node);
		int len = nodes.getLength();
		List<SwitchDefine<T>> defines = new ArrayList<SwitchDefine<T>>();
		for(int i = 0; i < len; i++) {
			Node switchNode = nodes.item(i);
			SwitchDefine<T> define = null;
			Node defaultSwitchNode = getNode("./default-node", switchNode);
			if (defaultSwitchNode != null) {
				String nextNodeName = defaultSwitchNode.getTextContent();
				define = new SwitchDefine<T>(constants.getTrueCondition(), nextNodeName);
			} else {
				String conditionClassName = getNodeContent("./condition/@class", switchNode);
				String parameterClassName = null;
				Node parameterClassNode = getNode("./condition/parameter", switchNode);
				if (parameterClassNode != null) {
					parameterClassName = parameterClassNode.getTextContent();
				}
				SwitchCondition<T> condition = makeSwitchCondition(conditionClassName, parameterClassName);
				String nextNodeName = getNodeContent("./next-node", switchNode);
				define = new SwitchDefine<T>(condition, nextNodeName);
			}
			defines.add(define);
		}
		String name = node.getAttributes().getNamedItem("name").getNodeValue();
		WorkflowNodeEntry<T> entry = new SwitchNodeEntry<T>(name, this, defines);
		map.put(name, entry);
	}
	
	private void initModuleNode(Node node) throws DOMException, XPathExpressionException {
		String classpath = getNodeContent("./ref-classpath", node);
		InputStream in = WorkflowConfigure.class.getResourceAsStream(classpath);
		WorkflowNode<T> moduleNode = new ModuleWorkflowNode<T>(in);
		String nextNodeName = getNodeContent("./next-node", node);
		WorkflowNodeEntry<T> entry = new SequenceNodeEntry<T>(this, moduleNode, nextNodeName);
		String name = node.getAttributes().getNamedItem("name").getNodeValue();
		map.put(name, entry);
	}
	
	@SuppressWarnings("unchecked")
	private WorkflowNode<T> makeSequenceNode(String className, String parameterClassName) {
		try {
			if (parameterClassName != null) {
				// iterate the constructors to find one's parameter type is suitable for parameter class
				Object parameterObject = Class.forName(parameterClassName).newInstance();
				Constructor<?>[] cons = Class.forName(className).getConstructors();
				for(int i = 0; i < cons.length; i++) {
					Class<?>[] classes = cons[i].getParameterTypes();
					if (classes.length == 1 && classes[0].isInstance(parameterObject)) {
						return (WorkflowNode<T>) cons[i].newInstance(parameterObject);
					}
				}
				throw new WorkflowException("Cannot find a matching constructor to initialize workflow node: " + className + "(" + parameterClassName + ")");
			} else {
				return (WorkflowNode<T>) Class.forName(className).newInstance();
			}
		} catch (Exception e) {
			throw new WorkflowException(e);
		}
	}
	
	@SuppressWarnings("unchecked")
	private SwitchCondition<T> makeSwitchCondition(String conditionClassName, String parameterClassName) {
		try {
			if (parameterClassName != null) {
				// iterate the constructors to find one's parameter type is suitable for parameter class
				Object parameterObject = Class.forName(parameterClassName).newInstance();
				Constructor<?>[] cons = Class.forName(conditionClassName).getConstructors();
				for(int i = 0; i < cons.length; i++) {
					Class<?>[] classes = cons[i].getParameterTypes();
					if (classes.length == 1 && classes[0].isInstance(parameterObject)) {
						return (SwitchCondition<T>) cons[i].newInstance(parameterObject);
					}
				}
				throw new WorkflowException("Cannot find a matching constructor to initialize switch condition: " + conditionClassName + "(" + parameterClassName + ")");
			} else {
				return (SwitchCondition<T>) Class.forName(conditionClassName).newInstance();
			}
		} catch (Exception e) {
			throw new WorkflowException(e);
		}
	}
	
	private Node getNode(String exp) throws XPathExpressionException {
		return (Node) xpath.evaluate(exp, root, XPathConstants.NODE);
	}
	
	private Node getNode(String exp, Node node) throws XPathExpressionException {
		return (Node) xpath.evaluate(exp, node, XPathConstants.NODE);
	}
	
	private String getNodeContent(String exp) throws DOMException, XPathExpressionException {
		return getNode(exp).getTextContent();
	}
	
	private String getNodeContent(String exp, Node node) throws DOMException, XPathExpressionException {
		return getNode(exp, node).getTextContent();
	}
	
	private NodeList getNodeList(String exp) throws XPathExpressionException {
		return (NodeList) xpath.evaluate(exp, root, XPathConstants.NODESET);
	}
	
	private NodeList getNodeList(String exp, Node node) throws XPathExpressionException {
		return (NodeList) xpath.evaluate(exp, node, XPathConstants.NODESET);
	}
}
